package org.codefilarete.stalactite.query.builder;

import javax.annotation.Nullable;
import java.sql.PreparedStatement;
import java.util.HashMap;
import java.util.Map;

import org.codefilarete.stalactite.query.api.Fromable;
import org.codefilarete.stalactite.query.api.Selectable;
import org.codefilarete.stalactite.query.model.Placeholder;
import org.codefilarete.stalactite.query.model.ValuedVariable;
import org.codefilarete.stalactite.query.api.Variable;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.statement.PreparedSQL;
import org.codefilarete.stalactite.sql.statement.binder.ColumnBinderRegistry;
import org.codefilarete.stalactite.sql.statement.binder.ParameterBinder;
import org.codefilarete.tool.trace.MutableInt;

/**
 * An appender to a {@link PreparedSQL}
 */
public class PreparedSQLAppender implements SQLAppender {
	
	private final SQLAppender delegate;
	private final ColumnBinderRegistry parameterBinderRegistry;
	private final Map<Integer, ParameterBinder<?>> parameterBinders;
	private final Map<Integer, Object> values;
	private final MutableInt paramCounter;
	
	public PreparedSQLAppender(SQLAppender sqlAppender, ColumnBinderRegistry parameterBinderRegistry) {
		this(sqlAppender, parameterBinderRegistry, new HashMap<>(), new HashMap<>(), new MutableInt(1));
	}
	
	private PreparedSQLAppender(SQLAppender delegate,
								ColumnBinderRegistry parameterBinderRegistry,
								Map<Integer, ? extends ParameterBinder<?>> parameterBinders,
								Map<Integer, Object> values,
								MutableInt paramCounter) {
		this.delegate = delegate;
		this.parameterBinderRegistry = parameterBinderRegistry;
		this.parameterBinders = (Map<Integer, ParameterBinder<?>>) parameterBinders;
		this.values = values;
		this.paramCounter = paramCounter;
	}
	
	public Map<Integer, Object> getValues() {
		return values;
	}
	
	public Map<Integer, ParameterBinder<?>> getParameterBinders() {
		return parameterBinders;
	}
	
	@Override
	public PreparedSQLAppender cat(String s, String... ss) {
		delegate.cat(s, ss);
		return this;
	}
	
	/**
	 * Implemented such it adds the value as a {@link PreparedStatement} mark (?) and keeps it for future use in the value list.
	 *
	 * @param column
	 * @param value  the object to be added/printed to the statement
	 * @return this
	 * @param <V> value type
	 */
	@Override
	public <V> PreparedSQLAppender catValue(@Nullable Selectable<V> column, Object value) {
		ParameterBinder<?> parameterBinder;
		if (column == null) {
			parameterBinder = getParameterBinderFromRegistry(value);
		} else if (column instanceof Column) {
			parameterBinder = parameterBinderRegistry.getBinder((Column) column);
		} else {
			parameterBinder = parameterBinderRegistry.getBinder(column.getJavaType());
		}
		return catValue(value, parameterBinder);
	}
	
	@Override
	public PreparedSQLAppender catValue(Object value) {
		return catValue(value, getParameterBinderFromRegistry(value));
	}
	
	private ParameterBinder<?> getParameterBinderFromRegistry(Variable value) {
		ParameterBinder<?> parameterBinder = null;
		if (value instanceof ValuedVariable) {
			parameterBinder = getParameterBinderFromRegistry(((ValuedVariable) value).getValue());
		} else if (value instanceof Placeholder) {
			parameterBinder = parameterBinderRegistry.getBinder(((Placeholder) value).getValueType());
		}
		return parameterBinder;
	}	
	private ParameterBinder<?> getParameterBinderFromRegistry(Object value) {
		ParameterBinder<?> parameterBinder;
		if (value instanceof Variable) {
			parameterBinder = getParameterBinderFromRegistry(((Variable) value));
		} else {
			Class<?> binderType = value.getClass().isArray() ? value.getClass().getComponentType() : value.getClass();
			parameterBinder = parameterBinderRegistry.getBinder(binderType);
		}
		return parameterBinder;
	}
	
	private PreparedSQLAppender catValue(Object value, ParameterBinder<?> binderSupplier) {
		if (value instanceof ValuedVariable) {
			Object innerValue = ((ValuedVariable) value).getValue();
			if (innerValue instanceof Column) {
				// Columns are simply appended (no binder needed nor index increment)
				delegate.catColumn((Column) innerValue);
			} else {
				appendPlaceholder(innerValue, binderSupplier);
			}
		} else {
			appendPlaceholder(value, binderSupplier);
		}
		return this;
	}
	
	private void appendPlaceholder(Object value, ParameterBinder<?> binderSupplier) {
		delegate.cat("?");
		values.put(paramCounter.getValue(), value);
		parameterBinders.put(paramCounter.getValue(), binderSupplier);
		paramCounter.increment();
	}
	
	@Override
	public PreparedSQLAppender catColumn(Selectable<?> column) {
		// Columns are simply appended (no binder needed nor index increment)
		delegate.catColumn(column);
		return this;
	}
	
	@Override
	public SQLAppender catTable(Fromable table) {
		delegate.catTable(table);
		return this;
	}
	
	@Override
	public SQLAppender removeLastChars(int length) {
		delegate.removeLastChars(length);
		return this;
	}
	
	@Override
	public String getSQL() {
		return delegate.getSQL();
	}
	
	@Override
	public SubSQLAppender newSubPart(DMLNameProvider dmlNameProvider) {
		SQLAppender self = PreparedSQLAppender.this;
		return new DefaultSubSQLAppender(new PreparedSQLAppender(
				this.delegate.newSubPart(dmlNameProvider),
				this.parameterBinderRegistry,
				this.parameterBinders,
				this.values,
				this.paramCounter)) {
			@Override
			public SQLAppender close() {
				// nothing special
				return self;
			}
		};
	}
}
